﻿// Some of the code was based on some of Mike Krüger's code from the ICSharpCode.TextEditor.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Storm.TextEditor.Editor.Text;
using Storm.TextEditor.Parser.Objects;

namespace Storm.TextEditor.Editor.CodeCompletion
{
    [ToolboxItem(false)]
    public class CodeCompletionWindow : UserControl
    {
        private const int ScrollbarWidth = 16;
        private const int MaxListLength = 10;
        private readonly string[] RemoveCharacters = new string[] {
            "(",
            ")",
            ".",
            "[",
            "]",
            "{",
            "}",
            ",",
            "+",
            "-",
            "*",
            "/",
            "%",
            "="
        };

        private bool commitChanges = true;
        public bool CommitChanges
        {
            get { return commitChanges; }
            set { commitChanges = value; }
        }

        private int startIndex = -1;
        private int startRow = -1;
        private int endIndex = -1;
        private int endRow = -1;
        private TextEditor textEditor;
        private bool inScrollUpdate;
        private int tooltipDuration = 6000;
        private ToolTip toolTip = new ToolTip();
        private VScrollBar vScrollBar = new VScrollBar();
        private ICompletionDataProvider completionDataProvider;
        public CodeCompletionMode Mode { get; set; }
        public ICompletionDataProvider CompletionDataProvider
        {
            get { return completionDataProvider; }
            set
            {
                completionDataProvider = value;
                if (completionDataProvider != null)
                {
                    PopulateCompletionDataList();
                    if (CompletionDataProvider.CompletionData.Count > MaxListLength)
                    {
                        vScrollBar.Maximum = CompletionDataProvider.CompletionData.Count - 1;
                    }
                }
            }
        }

        public int TooltipDuration
        {
            get { return tooltipDuration; }
            set { tooltipDuration = value; }
        }

        private ImageList imageList;
        public ImageList ImageList
        {
            get { return imageList; }
            set { imageList = value; }
        }

        private int firstItem = 0;
        public int FirstItem
        {
            get { return firstItem; }
            set
            {
                firstItem = value;
                OnFirstItemChanged(EventArgs.Empty);
            }
        }

        private int selectedIndex = -1;
        public ICompletionData SelectedItem
        {
            get
            {
                if (selectedIndex < 0 || selectedIndex > CompletionDataProvider.CompletionData.Count - 1)
                    return null;

                return CompletionDataProvider.CompletionData[selectedIndex];
            }
        }

        public int SelectedIndex
        {
            get { return selectedIndex; }
        }

        public int ItemHeight
        {
            get { return Math.Max(imageList.ImageSize.Height, (int)(Font.Height * 1.25)); }
        }

        public int MaxVisibleItem
        {
            get { return Height / ItemHeight; }
        }

        public new bool Visible
        {
            get { return base.Visible; }
            set
            {
                base.Visible = value;
                if (base.Visible == false) Hide();
            }
        }

        public new void Show()
        {
            UpdateCompletionBounds();
            UpdateData();
            base.Show();
        }

        public void SortItems()
        {
            CompletionDataProvider.CompletionData.Sort(CompareCompletionData);
        }

        public void SelectIndex(int index)
        {
            int oldSelectedItem = selectedIndex;
            int oldFirstItem = firstItem;
            index = Math.Max(0, index);
            selectedIndex = Math.Max(0, Math.Min(CompletionDataProvider.CompletionData.Count - 1, index));
            if (selectedIndex < firstItem)
                FirstItem = selectedIndex;
            if (firstItem + MaxVisibleItem <= selectedIndex)
                FirstItem = selectedIndex - MaxVisibleItem + 1;
            Invalidate();
            if (Visible == false)
                UpdateCompletionBounds();
            Show();
            ShowTooltipForSelectedItem();
            OnSelectedItemChanged(EventArgs.Empty);
        }

        public void CenterViewOn(int index)
        {
            int oldFirstItem = this.FirstItem;
            firstItem = index - MaxVisibleItem / 2;
            if (firstItem < 0)
                this.FirstItem = 0;
            else if (firstItem >= CompletionDataProvider.CompletionData.Count - MaxVisibleItem)
                this.FirstItem = CompletionDataProvider.CompletionData.Count - MaxVisibleItem;
            else
                this.FirstItem = firstItem;

            if (this.FirstItem != oldFirstItem)
                Invalidate();
        }

        public void ClearSelection()
        {
            if (selectedIndex < 0)
                return;

            int itemNum = selectedIndex - firstItem;
            selectedIndex = -1;
            Invalidate(new Rectangle(0, itemNum * ItemHeight, Width, (itemNum + 1) * ItemHeight + 1));
            Update();
            OnSelectedItemChanged(EventArgs.Empty);
        }

        public void PageDown()
        {
            SelectIndex(selectedIndex + MaxVisibleItem);
        }

        public void PageUp()
        {
            SelectIndex(selectedIndex - MaxVisibleItem);
        }

        public void SelectNextItem()
        {
            SelectIndex(selectedIndex + 1);
        }

        public void SelectPrevItem()
        {
            SelectIndex(selectedIndex - 1);
        }

        public void SelectItemWithStart(string startText)
        {
            if (startText == null || startText.Length == 0) return;
            string originalStartText = startText;
            startText = startText.ToLower();
            int bestIndex = -1;
            for (int i = 0; i < CompletionDataProvider.CompletionData.Count; ++i)
            {
                string itemText = CompletionDataProvider.CompletionData[i].Text;
                string lowerText = itemText.ToLower();
                if (lowerText.StartsWith(startText) == true)
                {
                    bestIndex = i;
                    break;
                }
            }

            if (bestIndex < 0)
                ClearSelection();
            else
            {
                if (bestIndex < firstItem || firstItem + MaxVisibleItem <= bestIndex)
                {
                    SelectIndex(bestIndex);
                    CenterViewOn(bestIndex);
                }
                else
                    SelectIndex(bestIndex);
            }
        }

        internal bool CompleteCurrentItem(char inputKey)
        {
            bool show = false;
            if (this.Visible == true)
            {
                if (SelectedItem == null)
                {
                    this.Visible = false;
                    return show;
                }

                if (!SelectedItem.ShouldInsert(inputKey))
                {
                    return false;
                }

                int currentX = textEditor.Caret.Position.X;
                bool commit = CommitChanges;
                CommitChanges = false;
                if (endIndex != startIndex)
                {
                    TextRange removeRange = new TextRange(startIndex, startRow, endIndex, endRow);
                    textEditor.Document.DeleteRange(removeRange);
                }

                int characterLength = inputKey == '\0' || inputKey == '\n' || inputKey == '\r' ? 0 : 1;
                int x = textEditor.Caret.Position.X - characterLength > textEditor.Caret.CurrentRow.Text.Length - characterLength ? textEditor.Caret.CurrentRow.Text.Length - characterLength : textEditor.Caret.Position.X - characterLength;
                if (SelectedItem.CustomInsertionAction)
                {
                    SelectedItem.Insert(x, textEditor.Caret.Position.Y, textEditor);
                }
                else
                {
                    if (inputKey == '\t')
                    {
                        textEditor.Document.DeleteRange(new TextRange(startIndex, startRow, startIndex + 1, startRow));
                    }

                    textEditor.Document.InsertText(SelectedItem.Text, startIndex == -1 ? currentX - characterLength : startIndex, textEditor.Caret.Position.Y);
                    textEditor.Caret.Position.X = (startIndex == -1 ? currentX - 1 : startIndex) + SelectedItem.Text.Length + characterLength + (endIndex == startIndex ? 1 : 0);
                    textEditor.Caret.CropPosition();
                }

                CommitChanges = commit;
                Hide();
                toolTip.Hide(textEditor);
                CompletionDataProvider.OnCodeCompleted(inputKey, out show);
            }

            return show;
        }

        protected override void OnPaint(PaintEventArgs pe)
        {
            float yPos = 1;
            float itemHeight = ItemHeight;
            int imageWidth = Math.Min((int)(itemHeight * imageList.ImageSize.Width / imageList.ImageSize.Height), imageList.ImageSize.Width);
            int imageHeight = Math.Min(imageList.ImageSize.Height, (int)itemHeight);
            int curItem = Math.Max(0, firstItem);
            Graphics graphics = pe.Graphics;
            while (curItem < CompletionDataProvider.CompletionData.Count && yPos < Height)
            {
                RectangleF drawingBackground = new RectangleF(imageWidth + 1, yPos, Width - ScrollbarWidth - imageWidth - 2, itemHeight);
                if (drawingBackground.IntersectsWith(pe.ClipRectangle) == true)
                {
                    if (curItem == selectedIndex)
                    {
                        if (Mode != CodeCompletionMode.Suggestion)
                        {
                            using (LinearGradientBrush brush = new LinearGradientBrush(drawingBackground, Color.FromArgb(255, 252, 242), Color.FromArgb(255, 236, 181), LinearGradientMode.Vertical))
                            {
                                graphics.FillRectangle(brush, drawingBackground);
                            }
                        }

                        graphics.DrawRectangle(new Pen(Color.FromArgb(229, 195, 101)), new Rectangle((int)drawingBackground.X, (int)drawingBackground.Y, (int)drawingBackground.Width - 1, (int)drawingBackground.Height - 1));
                    }
                    else
                    {
                        graphics.FillRectangle(SystemBrushes.Window, drawingBackground);
                    }

                    int xPos = 0;
                    if (imageList != null && CompletionDataProvider.CompletionData[curItem].ImageIndex < imageList.Images.Count)
                    {
                        graphics.FillRectangle(SystemBrushes.Window, new Rectangle(0, (int)yPos, imageWidth + 1, imageHeight + 2));
                        graphics.DrawImage(imageList.Images[CompletionDataProvider.CompletionData[curItem].ImageIndex], new RectangleF(1, yPos, imageWidth, imageHeight));
                        xPos = imageWidth;
                    }

                    if (curItem == selectedIndex)
                    {
                        graphics.DrawString(CompletionDataProvider.CompletionData[curItem].Text, Font, SystemBrushes.WindowText, xPos + 2, yPos + 2);
                    }
                    else
                    {
                        graphics.DrawString(CompletionDataProvider.CompletionData[curItem].Text, Font, SystemBrushes.WindowText, xPos + 2, yPos + 2);
                    }
                }

                yPos += itemHeight;
                ++curItem;
            }

            graphics.DrawRectangle(SystemPens.Control, new Rectangle(0, 0, Width - 1, Height - 1));
        }

        protected override void OnMouseDown(MouseEventArgs e)
        {
            float yPos = 1;
            int curItem = firstItem;
            float itemHeight = ItemHeight;
            curItem = Math.Max(0, curItem);
            while (curItem < CompletionDataProvider.CompletionData.Count && yPos < Height)
            {
                RectangleF drawingBackground = new RectangleF(1, yPos, Width - 2, itemHeight);
                if (drawingBackground.Contains(e.X, e.Y))
                {
                    SelectIndex(curItem);
                    break;
                }

                yPos += itemHeight;
                ++curItem;
            }
        }

        public new void Hide()
        {
            startIndex = -1;
            startRow = -1;
            endIndex = -1;
            endRow = -1;
            if (CompletionDataProvider != null)
            {
                CompletionDataProvider.OnWindowHidden();
            }
            base.Hide();
        }

        protected override void OnMouseDoubleClick(MouseEventArgs e)
        {
            float yPos = 1;
            int curItem = firstItem;
            float itemHeight = ItemHeight;
            while (curItem < CompletionDataProvider.CompletionData.Count && yPos < Height)
            {
                RectangleF drawingBackground = new RectangleF(1, yPos, Width - 2, itemHeight);
                if (drawingBackground.Contains(e.X, e.Y))
                {
                    SelectIndex(curItem);
                    CompleteCurrentItem('\0');
                    break;
                }

                yPos += itemHeight;
                ++curItem;
            }
        }

        private string GetLastWord()
        {
            string keywordResult = "";
            int keywordArrayIndex = 0;
            int keywordStartIndex = 0;
            string[] keywords = textEditor.Caret.CurrentRow.Text.Split(' ');
            Array.ForEach(keywords, keyword =>
            {
                if (keywordStartIndex == textEditor.Selection.Bounds.FirstColumn || keywordStartIndex + keyword.Length == textEditor.Selection.Bounds.FirstColumn)
                {
                    if (keywordArrayIndex >= 0)
                    {
                        keywordResult = keywords[keywordArrayIndex].Substring(0, keywords[keywordArrayIndex].Length);
                        Array.ForEach(RemoveCharacters, removeCharacter =>
                        {
                            int charIndex = keywordResult.IndexOf(removeCharacter);
                            if (charIndex > -1)
                                keywordResult = keywordResult.Substring(charIndex + 1);
                        });
                    }
                }

                keywordStartIndex += keyword.Length + 1;
                keywordArrayIndex++;
            });

            return keywordResult;
        }

        private void UpdateCompletionBounds()
        {
            vScrollBar.Maximum = CompletionDataProvider.CompletionData.Count - 1;
            if (!Visible)
            {
                TextPoint caretPosition = textEditor.Caret.Position;
                Point location = textEditor.ActiveViewControl.GetPixelAtTextPoint(new TextPoint(caretPosition.X + 2, caretPosition.Y));
                location.Y += textEditor.ActiveViewControl.ViewPoint.RowHeight;
                Rectangle bounds = new Rectangle(location, Size);
                if (bounds.X + bounds.Width > textEditor.ActiveViewControl.ViewPoint.ClientAreaWidth + textEditor.ActiveViewControl.ViewPoint.TextMargin - 1)
                {
                    bounds.X = textEditor.ActiveViewControl.ViewPoint.ClientAreaWidth + textEditor.ActiveViewControl.ViewPoint.TextMargin - bounds.Width - 2;
                }

                if (bounds.Y + bounds.Height > textEditor.ActiveViewControl.ClientHeight - 1)
                {
                    bounds.Y = textEditor.ActiveViewControl.ClientHeight - bounds.Height - 2;
                }

                if (bounds.Y < location.Y && bounds.Y + bounds.Height > location.Y)
                {
                    bounds.Y = location.Y - bounds.Height - textEditor.ActiveViewControl.ViewPoint.RowHeight - 2;
                }

                this.Bounds = bounds;
                BringToFront();
            }
        }

        internal void UpdateKeyInput(Keys inputKey, bool control, bool alt, bool shift)
        {
            if (CompletionDataProvider == null)
            {
                return;
            }

            Refresh();
            switch (inputKey)
            {
                case Keys.Delete:
                    break;
                case Keys.Up:
                    if (SelectedIndex > 0)
                    {
                        SelectPrevItem();
                    }

                    break;
                case Keys.Down:
                    if (SelectedIndex < CompletionDataProvider.CompletionData.Count - 1)
                    {
                        SelectNextItem();
                    }

                    break;
            }
        }

        public void Reset()
        {
            startIndex = -1;
            startRow = -1;
            endIndex = -1;
            endRow = -1;
        }

        internal void UpdateInput(char inputKey, bool control, bool alt, bool shift)
        {
            if (CompletionDataProvider == null)
            {
                return;
            }

            int index = textEditor.Caret.Position.X;
            int row = textEditor.Caret.Position.Y;
            Refresh();
            switch (inputKey)
            {
                case '(':
                    if (Mode == CodeCompletionMode.Suggestion)
                    {
                        return;
                    }

                    this.CompleteCurrentItem(inputKey);
                    this.ShowTooltipForSelectedItem();
                    break;
                case ')':
                    if (Mode == CodeCompletionMode.Suggestion)
                    {
                        return;
                    }

                    this.CompleteCurrentItem(')');
                    toolTip.Hide(textEditor);
                    break;
                case '.':
                case ',':
                case '?':
                case '"':
                case ';':
                case '+':
                case '-':
                case '*':
                case '\'':
                case ':':
                case '=':
                case '[':
                case ']':
                case '{':
                case '}':
                case (char)Keys.Tab:
                case (char)Keys.Enter:
                case (char)Keys.Space:
                    if (Mode == CodeCompletionMode.Suggestion && inputKey != (char)Keys.Tab)
                    {
                        return;
                    }

                    if (this.CompleteCurrentItem(inputKey) && CompletionDataProvider.CompletionData.Count > 0)
                    {
                        SelectIndex(0);
                    }

                    if (inputKey == '.')
                    {
                        this.UpdateCompletionBounds();
                        this.Visible = true;
                        Invalidate();
                        if (CommitChanges == true)
                        {
                            startIndex = index;
                            startRow = row;
                            endIndex = index;
                            endRow = row;
                        }
                    }
                    else if (inputKey == ',')
                    {
                        this.ShowTooltipForSelectedItem();
                        Hide();
                    }
                    else
                    {
                        Hide();
                    }

                    break;
                default:
                    if (inputKey == (char)Keys.Back)
                    {
                        if (Visible == false || index < startIndex)
                        {
                            Hide();
                            return;
                        }
                    }

                    if (textEditor.Caret.CurrentRow.Text.Length > 0)
                    {
                        char character = textEditor.Caret.CurrentRow.Text[Math.Max(0, index - 2)];
                        if (char.IsLetter(character) && !Visible)
                        {
                            Hide();
                            return;
                        }

                        if (CompletionDataProvider.UseAllowShow && CompletionDataProvider.AllowShow == false)
                        {
                            Hide();
                            return;
                        }

                        if (CommitChanges == true)
                        {
                            if (startIndex == -1) startIndex = index - 1;
                            if (startRow == -1) startRow = row;
                            if (endIndex == -1) endIndex = index;
                            if (endRow == -1) endRow = row;
                        }

                        if (CommitChanges == true)
                        {
                            endIndex = index;
                            endRow = row;
                        }

                        if (startIndex == -1 || startRow == -1 || endRow == -1 || endIndex == -1)
                        {
                            return;
                        }

                        bool cancel = false;
                        if (!CompletionDataProvider.UseAllowShow) CompletionDataProvider.OnBeforeShow(inputKey, out cancel);
                        if (!cancel) SelectItemWithStart(textEditor.Document.GetRange(new TextRange(startIndex, startRow, index, row)));
                    }

                    break;
            }
        }

        private void OnTextEditorMouseDown(object sender, MouseEventArgs e)
        {
            this.Visible = false;
            toolTip.Hide(textEditor);
            textEditor.Focus();
        }

        private static int CompareCompletionData(ICompletionData a, ICompletionData b)
        {
            if (a == null)
                throw new ArgumentNullException("a");

            if (b == null)
                throw new ArgumentNullException("b");

            return a.Text.CompareTo(b.Text);
        }

        private void VScrollBarValueChanged(object sender, EventArgs e)
        {
            if (inScrollUpdate) return;
            inScrollUpdate = true;
            FirstItem = vScrollBar.Value;
            Refresh();
            inScrollUpdate = false;
        }

        public void ShowTooltipForSelectedItem()
        {
            if (SelectedItem != null && Visible)
            {
                Point position = this.Location;
                position.X += (this.Width + 2);
                toolTip.Show(SelectedItem.Description, textEditor, position.X, position.Y, tooltipDuration);
                textEditor.Focus();
            }
        }

        private void UpdateData()
        {
            if (CompletionDataProvider != null)
            {
                ImageList = CompletionDataProvider.ImageList;
                this.Size = new Size(243, ItemHeight * MaxListLength);
                CompletionDataProvider.CompletionData.Sort(CompareCompletionData);
                vScrollBar.Maximum = CompletionDataProvider.CompletionData.Count - 1;
                if (vScrollBar.Value > vScrollBar.Maximum)
                {
                    vScrollBar.Value = 0;
                }
            }
        }

        public void PopulateCompletionDataList()
        {
            CompletionDataProvider.CreateCompletionData();
            UpdateData();
        }

        public void UpdateCompletionDataList()
        {
            UpdateData();
        }

        protected virtual void OnSelectedItemChanged(EventArgs e)
        {
            ShowTooltipForSelectedItem();
            if (SelectedItem != null)
            {
                CompletionDataProvider.OnSelectedItemChanged(SelectedItem);
            }

            if (SelectedItemChanged != null)
            {
                SelectedItemChanged(this, e);
            }
        }

        protected virtual void OnFirstItemChanged(EventArgs e)
        {
            if (inScrollUpdate) return;
            inScrollUpdate = true;
            vScrollBar.Value = Math.Min(vScrollBar.Maximum, FirstItem);
            inScrollUpdate = false;
            if (FirstItemChanged != null)
            {
                FirstItemChanged(this, e);
            }
        }

        public event EventHandler SelectedItemChanged;
        public event EventHandler FirstItemChanged;

        public CodeCompletionWindow(TextEditor textEditor)
        {
            SetStyle(ControlStyles.UserPaint, true);
            SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
            this.textEditor = textEditor;
            this.textEditor.MouseDown += OnTextEditorMouseDown;
            this.Font = new Font("Segoe UI", 8);
            this.Size = new Size(1, 1);
            this.Cursor = Cursors.Default;
            vScrollBar = new VScrollBar();
            vScrollBar.Dock = DockStyle.Right;
            vScrollBar.Minimum = 0;
            vScrollBar.SmallChange = 1;
            vScrollBar.LargeChange = MaxListLength;
            vScrollBar.ValueChanged += VScrollBarValueChanged;
            this.Controls.Add(vScrollBar);
        }
    }
}
